Skip to content

Add benchmark budget enforcement with CI reporting - #9150

Draft
joseph-isaacs wants to merge 3 commits into
developfrom
claude/codspeed-benchmark-ci-check-d50h60
Draft

Add benchmark budget enforcement with CI reporting#9150
joseph-isaacs wants to merge 3 commits into
developfrom
claude/codspeed-benchmark-ci-check-d50h60

Conversation

@joseph-isaacs

Copy link
Copy Markdown
Contributor

Rationale for this change

The benchmarking guide requires that each benchmark iteration complete in less than 1ms to keep benchmarks snappy locally and on CI. However, CodSpeed's simulation instrument reports estimated cycles from instruction traces rather than wall-clock time, so it cannot enforce this rule.

This change adds automated budget checking that:

  1. Rebuilds benchmarks in CodSpeed's walltime mode to measure actual per-iteration execution time
  2. Compares the fastest observed iteration (least affected by runner noise) against the 1ms budget
  3. Reports violations as a PR comment rather than failing the build
  4. Automatically excludes benchmarks gated with #[cfg(not(codspeed))] by only checking benchmarks CodSpeed actually measures

This closes the gap between the documented requirement and CI enforcement.

What changes are included in this PR?

New script: scripts/check-bench-budget.py

  • check subcommand: Reads raw walltime results from one shard, filters to benchmarks CodSpeed measures, and emits a JSON verdict
  • report subcommand: Merges per-shard verdicts into a Markdown comment body
  • Utility functions for parsing CodSpeed's analysis-mode output and formatting durations

New tests: scripts/tests/test_check_bench_budget.py

  • Unit tests for duration formatting, scope parsing, violation detection, and report rendering
  • Tests verify correct handling of multi-shard results, truncation of large regressions, and filtering of out-of-scope benchmarks

CI workflow updates: .github/workflows/codspeed.yml

  • Added run-budget-check output to the changes job to detect when benchmarks are modified
  • New bench-budget job that runs on 8 shards (same sharding as the main CodSpeed job):
    • Builds benchmarks in walltime mode
    • Runs them with CODSPEED_ENV=1 to trigger JSON output from the divan harness
    • Checks results against the budget using the new script
  • New bench-budget-comment job that merges verdicts and posts a sticky PR comment

Documentation: docs/developer-guide/benchmarking.md

  • Added explanation of how the budget check works
  • Included reproduction steps for running the check locally
  • Clarified that #[cfg(not(codspeed))] benchmarks are automatically exempt

CI test: .github/workflows/ci.yml

  • Added pytest invocation for the new benchmark budget tests

What APIs are changed? Are there any user-facing changes?

No public APIs are changed. This is purely a CI/tooling addition.

User-facing changes:

  • Pull requests that modify benchmarks will now receive a comment reporting any benchmarks that exceed the 1ms per-iteration budget
  • Developers can reproduce the check locally using the documented commands
  • The check is informational (does not fail the build) to allow for discussion of legitimate cases where a slower benchmark is acceptable

https://claude.ai/code/session_014WMg4dzviJZfD3tZgmcNty

claude added 3 commits August 3, 2026 10:49
…dget

The benchmarking guide asks that each benchmark iteration complete in under
1ms, but nothing enforced it. CodSpeed runs in simulation mode, which estimates
cycles from an instruction trace and never reports wall-clock time, so the
existing job cannot answer the question.

The `divan` dependency is really `codspeed-divan-compat`. Built without
`--cfg codspeed` it re-exports CodSpeed's patched divan, which dumps
per-iteration statistics as JSON when `CODSPEED_ENV` is set. The new
`bench-budget` job rebuilds the sharded benchmarks in walltime mode, runs them
once outside the CodSpeed runner, and compares the fastest observed iteration
against the budget. Using the minimum rather than the median keeps a loaded
shared runner from making the check flaky.

Only benchmarks CodSpeed actually measures are checked. That set comes from
replaying the analysis-mode binaries, which print one line per benchmark they
run; because that is the same build CodSpeed uses, benchmarks gated with
`#[cfg(not(codspeed))]` are excluded automatically rather than through an
allowlist that would drift. Both modes generate benchmark URIs with identical
code, so the two sets join exactly.

Results are posted as a sticky PR comment rather than failing the build, and
the job only runs on pull requests that touch a `benches/` directory.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
Adds a benchmark that deliberately breaks the 1ms per-iteration rule, so the
bench-budget job has something to catch on this pull request. Counting set bits
across a 32 MiB bit buffer is memory-bandwidth bound and measures 3.28ms
locally, against 71 existing vortex-mask benchmarks that all pass.

The work is real rather than a sleep: CodSpeed's simulation instrument excludes
system calls, so a sleeping benchmark would appear free there while still
consuming CI wall-clock time.

This commit must be reverted before merge.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

⏱️ Benchmark iteration budget

⚠️ 30 of 1847 CodSpeed benchmarks exceed the 1 ms per-iteration budget.

CodSpeed's simulation instrument runs each benchmark exactly once, so a slow iteration costs CI time without buying any extra signal. Shrink the input size, or gate the benchmark with #[cfg(not(codspeed))]. See the benchmarking guide.

Benchmark Fastest iteration Over budget
vortex-array/benches/list_sum.rs::list_sum_nullable_elements_large 83.7 ms 83.7×
vortex-array/benches/list_sum.rs::list_sum_large 14.4 ms 14.4×
vortex-array/benches/list_sum.rs::listview_sum_large 13.6 ms 13.6×
vortex/benches/single_encoding_throughput.rs::zstd_compress_u32 9.6 ms 9.6×
vortex/benches/single_encoding_throughput.rs::zstd_compress_string 8.35 ms 8.3×
encodings/fsst/benches/fsst_like.rs::fsst_contains[log] 5.78 ms 5.8×
encodings/fsst/benches/fsst_like.rs::fsst_contains[rare] 5 ms 5.0×
vortex-array/benches/list_sum.rs::fsl_sum_large 4.18 ms 4.2×
encodings/fsst/benches/fsst_like.rs::fsst_contains[cb] 3.28 ms 3.3×
encodings/fsst/benches/fsst_like.rs::fsst_contains[json] 2.81 ms 2.8×
encodings/fsst/benches/fsst_url_compare.rs::like_substr_low_match 1.85 ms 1.8×
vortex-row/benches/row_encode.rs::struct_mixed_arrow_row 1.8 ms 1.8×
encodings/fsst/benches/fsst_compress.rs::compress_fsst[(10000, 64, 8)] 1.69 ms 1.7×
encodings/fsst/benches/fsst_compress.rs::chunked_canonicalize_into[(10, 10000, 64, 4)] 1.61 ms 1.6×
encodings/fsst/benches/fsst_url_compare.rs::like_substr_high_match 1.57 ms 1.6×
encodings/fsst/benches/fsst_compress.rs::chunked_into_canonical[(10, 10000, 64, 4)] 1.53 ms 1.5×
vortex-row/benches/row_encode.rs::struct_mixed_vortex 1.46 ms 1.5×
encodings/fsst/benches/chunked_dict_fsst_builder.rs::chunked_dict_fsst_into_canonical[(1000, 10, 100)] 1.42 ms 1.4×
encodings/fsst/benches/chunked_dict_fsst_builder.rs::chunked_dict_fsst_canonical_into[(1000, 10, 100)] 1.41 ms 1.4×
encodings/fsst/benches/chunked_dict_fsst_builder.rs::chunked_dict_fsst_into_canonical[(1000, 1000, 100)] 1.39 ms 1.4×
encodings/fsst/benches/chunked_dict_fsst_builder.rs::chunked_dict_fsst_into_canonical[(1000, 100, 100)] 1.38 ms 1.4×
encodings/fsst/benches/chunked_dict_fsst_builder.rs::chunked_dict_fsst_canonical_into[(1000, 1000, 100)] 1.38 ms 1.4×
encodings/fsst/benches/chunked_dict_fsst_builder.rs::chunked_dict_fsst_canonical_into[(1000, 100, 100)] 1.38 ms 1.4×
encodings/fsst/benches/fsst_compress.rs::chunked_into_canonical[(1000, 100, 16, 64)] 1.36 ms 1.4×
encodings/fsst/benches/fsst_compress.rs::chunked_canonicalize_into[(1000, 100, 16, 64)] 1.29 ms 1.3×
encodings/fsst/benches/fsst_compress.rs::chunked_into_canonical[(1000, 100, 16, 16)] 1.23 ms 1.2×
encodings/fsst/benches/fsst_compress.rs::chunked_canonicalize_into[(1000, 100, 16, 16)] 1.19 ms 1.2×
encodings/fsst/benches/fsst_compress.rs::chunked_into_canonical[(1000, 100, 16, 4)] 1.14 ms 1.1×
vortex/benches/single_encoding_throughput.rs::zstd_decompress_string 1.07 ms 1.1×
encodings/fsst/benches/fsst_compress.rs::chunked_canonicalize_into[(1000, 100, 16, 4)] 1.05 ms 1.1×
How this is measured

Benchmarks are rebuilt in CodSpeed's walltime mode and run once outside the CodSpeed runner. The reported number is min_ns -- the fastest observed iteration, which is the estimate least contaminated by runner noise, so a shared CI machine cannot make this check flaky.

Only benchmarks that CodSpeed actually measures are checked. That set comes from the analysis-mode binaries built by cargo codspeed build, which enumerate every benchmark they run, so anything behind #[cfg(not(codspeed))] is excluded automatically rather than by an allowlist that can drift.

@codspeed-hq

codspeed-hq Bot commented Aug 3, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 14.22%

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

❌ 1 regressed benchmark
✅ 1884 untouched benchmarks
🆕 1 new benchmark
⏩ 12 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime cuda/bitpacked_u8/unpack/3bw[100M] 299.9 µs 349.6 µs -14.22%
🆕 Simulation over_budget_prefix_count N/A 16.2 ms N/A

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/codspeed-benchmark-ci-check-d50h60 (40c3940) with develop (aa7cd40)

Open in CodSpeed

Footnotes

  1. 12 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants